// ==UserScript==
// @name         Transsion AI 参数导出器
// @namespace    local.transsion-ai-tools
// @version      1.5.0
// @description  从 gpt.transsion.com 当前对话窗口导出 txcodex.json 所需参数，运行时抓取真实请求头，避免 tenant/topic 抓错。
// @author       Codex
// @match        https://gpt.transsion.com/*
// @match        https://*.transsion.com/*
// @match        http://*.transsion.com/*
// @grant        GM_setClipboard
// @grant        GM_download
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

(function () {
  "use strict";

  const DEFAULT_CONFIG = {
    device_name: "A",
    chat_topic_id: "1092159",
    p_auth: "",
    p_rtoken: "",
    p_empno: "",
    p_appid: "c_MjMwMzIxMDAydw",
    p_langid: "zh",
    p_platform: "win",
    p_syscode: "",
    tenant: "b4b38189e32f48fca1a29da4a7a56580",
    model_code: "97",
    model_id: "97",
    speech_id: "1",
    memory_enabled: true,
    clip_prefix: "[CLIP]",
    poll_interval: 2,
    ticket_url: "https://pfgateway.transsion.com:9199/transsioner-intelligent-service/ticket/getWsTicket",
    history_url: "https://pfgateway.transsion.com:9199/transsioner-intelligent-service/chatTopic/getPageHistory/{topic_id}",
    ws_url: "wss://gpt-ws.transsion.com/",
    create_topic_url: "https://pfgateway.transsion.com:9199/transsioner-intelligent-service/chatTopic",
    max_steps: 0
  };

  const AUTH_KEY_RE = /^tr_t_c_/i;
  const RTOKEN_KEY_RE = /^tr_rt_c_/i;
  const AUTH_VALUE_RE = /^r_[A-Za-z0-9+/=_-]{20,}$/;
  const RTOKEN_VALUE_RE = /^u_[A-Za-z0-9+/=_-]{20,}$/;
  const APPID_RE = /^c_[A-Za-z0-9+/=_-]{8,}$/;
  const TENANT_RE = /^[a-f0-9]{32}$/i;
  const TOPIC_URL_RE = /(?:chatTopicId|topicId|chat_topic_id)[=/?:&]+([1-9][0-9]{3,15})/i;
  const runtimeState = {
    p_auth: "",
    p_rtoken: "",
    p_empno: "",
    p_appid: "",
    tenant: "",
    chat_topic_id: "",
    model_code: "",
    model_id: "",
    speech_id: "",
    memory_enabled: null
  };

  function rememberHeader(name, value) {
    if (!name || value == null) return;
    const key = String(name).toLowerCase();
    const text = String(value).trim();
    if (!text) return;
    if (key === "p-auth" && AUTH_VALUE_RE.test(text)) runtimeState.p_auth = text;
    if (key === "p-rtoken" && RTOKEN_VALUE_RE.test(text)) runtimeState.p_rtoken = text;
    if (key === "p-empno") runtimeState.p_empno = text;
    if (key === "p-appid" && APPID_RE.test(text)) runtimeState.p_appid = text;
    if (key === "x-header-tenant" && TENANT_RE.test(text)) runtimeState.tenant = text.toLowerCase();
  }

  function rememberUrl(url) {
    if (!url) return;
    const text = String(url);
    const topic = text.match(TOPIC_URL_RE);
    if (topic) runtimeState.chat_topic_id = topic[1];
  }

  function rememberPayload(payload) {
    if (payload == null) return;
    if (typeof payload !== "string") return;
    const json = decodeJsonMaybe(payload);
    if (!json || typeof json !== "object") return;
    walkJson(json, (key, value) => {
      if (value == null) return;
      const text = String(value).trim();
      if (!text) return;
      if ((key === "modelCode" || key === "model_code") && /^[0-9A-Za-z_-]{1,40}$/.test(text)) {
        runtimeState.model_code = text;
      }
      if ((key === "modelId" || key === "model_id") && /^[0-9A-Za-z_-]{1,40}$/.test(text)) {
        runtimeState.model_id = text;
        if (!runtimeState.model_code) runtimeState.model_code = text;
      }
      if ((key === "speechId" || key === "speech_id") && /^[0-9A-Za-z_-]{1,40}$/.test(text)) {
        runtimeState.speech_id = text;
      }
      if (key === "memoryEnabled" || key === "memory_enabled") {
        runtimeState.memory_enabled = Boolean(value);
      }
      if ((key === "chatTopicId" || key === "topicId" || key === "chat_topic_id") && /^[1-9][0-9]{3,15}$/.test(text)) {
        runtimeState.chat_topic_id = text;
      }
    });
  }

  function rememberHeaders(headers) {
    if (!headers) return;
    try {
      if (headers instanceof Headers) {
        headers.forEach((value, key) => rememberHeader(key, value));
        return;
      }
    } catch (_) {}
    if (Array.isArray(headers)) {
      headers.forEach((pair) => pair && rememberHeader(pair[0], pair[1]));
      return;
    }
    if (typeof headers === "object") {
      Object.entries(headers).forEach(([key, value]) => rememberHeader(key, value));
    }
  }

  function installRuntimeCapture() {
    const rawFetch = window.fetch;
    if (typeof rawFetch === "function" && !rawFetch.__txAiWrapped) {
      const wrappedFetch = function (input, init) {
        try {
          const url = typeof input === "string" ? input : input && input.url;
          rememberUrl(url);
          if (input && input.headers) rememberHeaders(input.headers);
          if (init && init.headers) rememberHeaders(init.headers);
          if (init && init.body) rememberPayload(init.body);
        } catch (_) {}
        return rawFetch.apply(this, arguments);
      };
      wrappedFetch.__txAiWrapped = true;
      window.fetch = wrappedFetch;
    }

    const RawXHR = window.XMLHttpRequest;
    if (typeof RawXHR === "function" && !RawXHR.__txAiWrapped) {
      const rawOpen = RawXHR.prototype.open;
      const rawSetRequestHeader = RawXHR.prototype.setRequestHeader;
      const rawSend = RawXHR.prototype.send;
      RawXHR.prototype.open = function (method, url) {
        try {
          this.__txAiUrl = url;
          rememberUrl(url);
        } catch (_) {}
        return rawOpen.apply(this, arguments);
      };
      RawXHR.prototype.setRequestHeader = function (name, value) {
        try {
          rememberHeader(name, value);
        } catch (_) {}
        return rawSetRequestHeader.apply(this, arguments);
      };
      RawXHR.prototype.send = function (body) {
        try {
          rememberPayload(body);
        } catch (_) {}
        return rawSend.apply(this, arguments);
      };
      RawXHR.__txAiWrapped = true;
    }

    const RawWebSocket = window.WebSocket;
    if (typeof RawWebSocket === "function" && !RawWebSocket.__txAiWrapped) {
      const WrappedWebSocket = function (url, protocols) {
        try {
          rememberUrl(url);
        } catch (_) {}
        const ws = protocols === undefined ? new RawWebSocket(url) : new RawWebSocket(url, protocols);
        try {
          const rawSend = ws.send;
          ws.send = function (data) {
            try {
              rememberPayload(data);
            } catch (_) {}
            return rawSend.apply(this, arguments);
          };
        } catch (_) {}
        return ws;
      };
      WrappedWebSocket.prototype = RawWebSocket.prototype;
      Object.setPrototypeOf(WrappedWebSocket, RawWebSocket);
      WrappedWebSocket.__txAiWrapped = true;
      window.WebSocket = WrappedWebSocket;
    }
  }

  function decodeJsonMaybe(value) {
    if (typeof value !== "string") return null;
    const trimmed = value.trim();
    if (!trimmed || !/^[\[{"]/.test(trimmed)) return null;
    try {
      return JSON.parse(trimmed);
    } catch (_) {
      return null;
    }
  }

  function walkJson(value, visitor, path = []) {
    if (value == null) return;
    if (Array.isArray(value)) {
      value.forEach((item, index) => walkJson(item, visitor, path.concat(index)));
      return;
    }
    if (typeof value === "object") {
      Object.entries(value).forEach(([key, item]) => {
        visitor(key, item, path.concat(key));
        walkJson(item, visitor, path.concat(key));
      });
    }
  }

  function readLocalStorage() {
    const entries = [];
    for (let i = 0; i < localStorage.length; i += 1) {
      const key = localStorage.key(i);
      entries.push([key, localStorage.getItem(key)]);
    }
    return entries;
  }

  function firstNonEmpty(...values) {
    return values.find((value) => typeof value === "string" && value.trim()) || "";
  }

  function findByJsonFields(entries) {
    const found = {};
    const fieldMap = {
      empno: "p_empno",
      empNo: "p_empno",
      employeeNo: "p_empno",
      employeeCode: "p_empno",
      account: "p_empno",
      appid: "p_appid",
      appId: "p_appid",
      tenant: "tenant",
      tenantId: "tenant",
      chatTopicId: "chat_topic_id",
      topicId: "chat_topic_id",
      modelCode: "model_code",
      modelId: "model_id",
      speechId: "speech_id"
    };

    for (const [, rawValue] of entries) {
      const json = decodeJsonMaybe(rawValue);
      if (!json) continue;
      walkJson(json, (key, value) => {
        if (typeof value !== "string" && typeof value !== "number") return;
        const mapped = fieldMap[key];
        if (!mapped || found[mapped]) return;
        const text = String(value).trim();
        if (mapped === "tenant" && !TENANT_RE.test(text)) return;
        if (mapped === "p_appid" && !APPID_RE.test(text)) return;
        found[mapped] = text;
      });
    }
    return found;
  }

  function extractParams() {
    const entries = readLocalStorage();
    const jsonFields = findByJsonFields(entries);

    let pAuth = runtimeState.p_auth || "";
    let pRtoken = runtimeState.p_rtoken || "";
    let pAppid = runtimeState.p_appid || jsonFields.p_appid || "";
    let tenant = runtimeState.tenant || jsonFields.tenant || "";
    let chatTopicId = runtimeState.chat_topic_id || jsonFields.chat_topic_id || "";
    let empno = runtimeState.p_empno || jsonFields.p_empno || "";
    let modelCode = runtimeState.model_code || jsonFields.model_code || "";
    let modelId = runtimeState.model_id || jsonFields.model_id || modelCode || "";
    let speechId = runtimeState.speech_id || jsonFields.speech_id || "";

    for (const [key, value] of entries) {
      const safeValue = String(value || "").trim();
      if (!pAuth && AUTH_KEY_RE.test(key) && AUTH_VALUE_RE.test(safeValue)) pAuth = safeValue;
      if (!pRtoken && RTOKEN_KEY_RE.test(key) && RTOKEN_VALUE_RE.test(safeValue)) pRtoken = safeValue;
      if (!pAppid && APPID_RE.test(safeValue)) pAppid = safeValue;
      if (!tenant && TENANT_RE.test(safeValue)) tenant = safeValue.toLowerCase();
    }

    const hrefTopic = location.href.match(TOPIC_URL_RE);
    if (!chatTopicId && hrefTopic) chatTopicId = hrefTopic[1];

    // 再做一轮宽松文本扫描，用于 localStorage 值是压缩字符串或嵌套文本的情况。
    // 注意：tenant 不能用“任意 32 位 hex”宽松匹配，localStorage 里有很多随机 id，
    // 错拿会导致 ticket 接口返回“租户未找到”。tenant 找不到时保留 DEFAULT_CONFIG。
    const allText = entries.map(([key, value]) => `${key}\n${value || ""}`).join("\n");
    pAuth = firstNonEmpty(pAuth, allText.match(/r_[A-Za-z0-9+/=_-]{20,}/)?.[0]);
    pRtoken = firstNonEmpty(pRtoken, allText.match(/u_[A-Za-z0-9+/=_-]{20,}/)?.[0]);
    pAppid = firstNonEmpty(pAppid, allText.match(/c_[A-Za-z0-9+/=_-]{8,}/)?.[0]);
    tenant = firstNonEmpty(
      tenant,
      allText.match(/(?:tenant|x-header-tenant|X-Header-Tenant)[^a-f0-9]{0,80}([a-f0-9]{32})/i)?.[1]
    );
    empno = firstNonEmpty(
      empno,
      allText.match(/"(?:empno|empNo|employeeNo|employeeCode|account)"\s*:\s*"([^"]{3,40})"/i)?.[1]
    );

    return {
      p_auth: pAuth,
      p_rtoken: pRtoken,
      p_empno: empno,
      p_appid: pAppid,
      tenant: tenant ? tenant.toLowerCase() : DEFAULT_CONFIG.tenant,
      chat_topic_id: chatTopicId,
      model_code: modelCode || modelId,
      model_id: modelId || modelCode,
      speech_id: speechId,
      memory_enabled: runtimeState.memory_enabled
    };
  }

  function buildConfig() {
    const extracted = extractParams();
    const config = { ...DEFAULT_CONFIG };
    Object.entries(extracted).forEach(([key, value]) => {
      if (value !== undefined && value !== null && String(value).trim() !== "") {
        config[key] = String(value).trim();
      }
    });
    return { config, extracted };
  }

  function mask(value) {
    if (!value) return "未找到";
    if (value.length <= 12) return value;
    return `${value.slice(0, 6)}...${value.slice(-6)}`;
  }

  function setStatus(message, isError = false) {
    const el = document.getElementById("tx-ai-exporter-status");
    if (!el) return;
    el.textContent = message;
    el.style.color = isError ? "#ff8f8f" : "#b9f7d0";
  }

  function refreshPreviewStatus() {
    const { config, extracted } = buildConfig();
    setStatus(
      `已监听页面请求。tenant=${mask(config.tenant)}，topic=${mask(config.chat_topic_id)}，model=${mask(config.model_code)}，P-Auth=${mask(extracted.p_auth || config.p_auth)}`,
      false
    );
  }

  async function copyConfig() {
    const { config, extracted } = buildConfig();
    const text = JSON.stringify(config, null, 2);
    const missing = ["p_auth", "p_rtoken", "p_empno"].filter((key) => !config[key]);

    try {
      if (typeof GM_setClipboard === "function") {
        GM_setClipboard(text, "text");
      } else {
        await navigator.clipboard.writeText(text);
      }
      setStatus(
        `已复制 JSON。P-Auth=${mask(extracted.p_auth)}，P-Rtoken=${mask(extracted.p_rtoken)}，工号=${mask(extracted.p_empno)}${missing.length ? "；缺少：" + missing.join(", ") : ""}`,
        missing.length > 0
      );
    } catch (error) {
      console.error(error);
      setStatus("复制失败，请看控制台或使用下载按钮。", true);
    }
  }

  function downloadConfig() {
    const { config, extracted } = buildConfig();
    const text = JSON.stringify(config, null, 2);
    const blob = new Blob([text + "\n"], { type: "application/json;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const filename = `transsion_ai_params_${new Date().toISOString().replace(/[:.]/g, "-")}.json`;

    if (typeof GM_download === "function") {
      GM_download({ url, name: filename, saveAs: true, onload: () => URL.revokeObjectURL(url) });
    } else {
      const a = document.createElement("a");
      a.href = url;
      a.download = filename;
      a.click();
      setTimeout(() => URL.revokeObjectURL(url), 1000);
    }
    setStatus(`已生成下载。P-Auth=${mask(extracted.p_auth)}，P-Rtoken=${mask(extracted.p_rtoken)}，工号=${mask(extracted.p_empno)}`);
  }

  function createButton(text, onClick) {
    const btn = document.createElement("button");
    btn.textContent = text;
    btn.type = "button";
    btn.style.cssText = [
      "border:0",
      "border-radius:8px",
      "padding:7px 10px",
      "font-size:12px",
      "font-weight:700",
      "cursor:pointer",
      "color:#102018",
      "background:#6ee7a8"
    ].join(";");
    btn.addEventListener("click", onClick);
    return btn;
  }

  function mountPanel() {
    if (!document.documentElement) return false;
    if (document.getElementById("tx-ai-exporter-host")) return true;

    const host = document.createElement("div");
    host.id = "tx-ai-exporter-host";
    host.style.cssText = [
      "position:fixed",
      "right:16px",
      "bottom:16px",
      "z-index:2147483647",
      "width:270px",
      "height:auto",
      "pointer-events:auto"
    ].join(";");
    const root = host.attachShadow ? host.attachShadow({ mode: "open" }) : host;
    const panel = document.createElement("div");
    panel.id = "tx-ai-exporter-panel";
    panel.style.cssText = [
      "width:270px",
      "box-sizing:border-box",
      "padding:12px",
      "border-radius:14px",
      "box-shadow:0 14px 40px rgba(0,0,0,.28)",
      "background:rgba(18,24,31,.94)",
      "backdrop-filter:blur(10px)",
      "color:#e9fff2",
      "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Arial,sans-serif"
    ].join(";");

    const title = document.createElement("div");
    title.textContent = "传音 AI 参数导出";
    title.style.cssText = "font-size:14px;font-weight:800;margin-bottom:8px;";

    const desc = document.createElement("div");
    desc.textContent = "只读取当前页 localStorage，不上传。复制后粘贴保存为 config.json 即可。";
    desc.style.cssText = "font-size:12px;line-height:1.45;color:#a8b8ad;margin-bottom:10px;";

    const row = document.createElement("div");
    row.style.cssText = "display:flex;gap:8px;margin-bottom:8px;";
    row.append(createButton("复制 JSON", copyConfig), createButton("下载 JSON", downloadConfig));

    const status = document.createElement("div");
    status.id = "tx-ai-exporter-status";
    status.textContent = "等待操作";
    status.style.cssText = "font-size:11px;line-height:1.4;color:#b9f7d0;word-break:break-all;";

    const close = document.createElement("button");
    close.textContent = "×";
    close.type = "button";
    close.title = "隐藏";
    close.style.cssText = "position:absolute;right:8px;top:6px;border:0;background:transparent;color:#9aaaa0;font-size:18px;cursor:pointer;";
    close.addEventListener("click", () => host.remove());

    panel.append(close, title, desc, row, status);
    root.appendChild(panel);
    document.documentElement.appendChild(host);
    return true;
  }

  function tryMountPanel() {
    if (mountPanel()) return;
    if (document.readyState === "loading") {
      document.addEventListener("DOMContentLoaded", mountPanel, { once: true });
    }
    window.addEventListener("load", mountPanel, { once: true });
    let tries = 0;
    const timer = setInterval(() => {
      tries += 1;
      if (mountPanel() || tries >= 20) clearInterval(timer);
    }, 500);
  }

  if (typeof GM_registerMenuCommand === "function") {
    GM_registerMenuCommand("显示传音 AI 参数导出浮窗", () => {
      const old = document.getElementById("tx-ai-exporter-host");
      if (old) old.remove();
      mountPanel();
    });
    GM_registerMenuCommand("复制传音 AI 参数 JSON", copyConfig);
    GM_registerMenuCommand("下载传音 AI 参数 JSON", downloadConfig);
  }

  installRuntimeCapture();
  console.log("[Transsion AI 参数导出器] userscript loaded:", location.href);
  tryMountPanel();
  setInterval(refreshPreviewStatus, 2500);
})();
